In [1]:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from scipy.stats import chi2_contingency
import plotly.express as px
import plotly.figure_factory as ff
from PIL import Image
import pynarrative as pn
import altair as alt

df = pd.read_csv("data_toxins__structures_merge_drop.csv")

print(df.shape)
(3678, 57)
In [2]:
print(df.columns)
Index(['id', 'common_name', 'description', 'cas', 'chemical_formula',
       'appearance', 'route_of_exposure', 'mechanism_of_toxicity',
       'metabolism', 'toxicity', 'lethaldose', 'carcinogenicity', 'use_source',
       'health_effects', 'symptoms', 'export', 'moldb_smiles', 'moldb_formula',
       'moldb_inchi', 'moldb_inchikey', 'moldb_average_mass', 'origin',
       'state', 'carcinogenicity_grouped', 'carcinogenicity_label',
       'types_all', 'locations_all', 'DATABASE_ID', 'DATABASE_NAME', 'SMILES',
       'INCHI_IDENTIFIER', 'INCHI_KEY', 'FORMULA', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_IUPAC', 'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE',
       'JCHEM_NUMBER_OF_RINGS', 'JCHEM_PHYSIOLOGICAL_CHARGE',
       'JCHEM_POLAR_SURFACE_AREA', 'JCHEM_REFRACTIVITY',
       'JCHEM_ROTATABLE_BOND_COUNT', 'JCHEM_RULE_OF_FIVE',
       'JCHEM_TRADITIONAL_IUPAC', 'JCHEM_VEBER_RULE', 'NAME', 'CAS',
       'SYNONYMS', 'TYPES', 'ID', 'smiles', 'ALOGPS_SOLUBILITY'],
      dtype='object')

Basandomi sul gruppo IARC di riferimento, per una migliore interpretazione dei dati aggrego e trasformo le precedenti classi di cancerogenicità:

0 = non classificato o non cancerogeno (Gruppo 3);

1 = cancerogeno certo (Gruppo 1) o possibile/probabile cancerogeno (Gruppi 2A/2B);

In [3]:
def convert_carcinogenicity(text):
    text = str(text).lower()

    if "group 1" in text or "1, carcinogenic to humans" in text:
        return 1
    elif "group 2a" in text or "2a, probably carcinogenic to humans" in text:
        return 1
    elif "group 2b" in text or "2b, possibly carcinogenic to humans" in text:
        return 1
    elif "group 3" in text or "3, not classifiable" in text:
        return 0
    elif "no indication" in text or "not listed by iarc" in text:
        return 0
    else:
        return 0  # default per altri casi non classificati

# Applica la funzione al DataFrame
df['carcinogenicity_score'] = df['carcinogenicity'].apply(convert_carcinogenicity)
In [4]:
unique_values3 = df["carcinogenicity_score"].unique()
print(unique_values3)
[1 0]
In [5]:
print(df.isna().sum())
id                                 0
common_name                        0
description                        0
cas                               50
chemical_formula                 136
appearance                       206
route_of_exposure                730
mechanism_of_toxicity            434
metabolism                       779
toxicity                        2510
lethaldose                      3267
carcinogenicity                    4
use_source                       662
health_effects                   780
symptoms                         814
export                             0
moldb_smiles                     135
moldb_formula                    132
moldb_inchi                      132
moldb_inchikey                   132
moldb_average_mass               132
origin                            10
state                             21
carcinogenicity_grouped            4
carcinogenicity_label              4
types_all                          4
locations_all                    163
DATABASE_ID                      159
DATABASE_NAME                    159
SMILES                           160
INCHI_IDENTIFIER                 159
INCHI_KEY                        159
FORMULA                          159
JCHEM_ACCEPTOR_COUNT             162
JCHEM_AVERAGE_POLARIZABILITY     162
JCHEM_BIOAVAILABILITY            164
JCHEM_DONOR_COUNT                162
JCHEM_FORMAL_CHARGE              159
JCHEM_GHOSE_FILTER               160
JCHEM_IUPAC                      166
JCHEM_LOGP                       169
JCHEM_MDDR_LIKE_RULE             164
JCHEM_NUMBER_OF_RINGS            164
JCHEM_PHYSIOLOGICAL_CHARGE       163
JCHEM_POLAR_SURFACE_AREA         162
JCHEM_REFRACTIVITY               164
JCHEM_ROTATABLE_BOND_COUNT       164
JCHEM_RULE_OF_FIVE               161
JCHEM_TRADITIONAL_IUPAC          166
JCHEM_VEBER_RULE                 164
NAME                             159
CAS                              168
SYNONYMS                         242
TYPES                            163
ID                               159
smiles                           159
ALOGPS_SOLUBILITY                794
carcinogenicity_score              0
dtype: int64
In [6]:
# Elimino variabili non informative o con troppi valori NA
df_clean = df.drop(columns=["toxicity", "lethaldose", "export", "DATABASE_ID", "ID", "DATABASE_NAME",
                            "ALOGPS_SOLUBILITY", "carcinogenicity_label", "TYPES"])
In [7]:
# Elimino i valori NA nelle variabili rimaste dopo la pulizia
df_clean = df_clean.dropna()
In [8]:
print(df_clean.shape)
(1790, 49)
In [9]:
df_clean.to_csv('df_clean.csv', index=False)
In [10]:
# Plot della distribuzione usando seaborn
plt.figure(figsize=(8, 6))
sns.countplot(x='carcinogenicity_score', data=df_clean, palette='pastel')
plt.xticks([0, 1], ['0 (non classificati)', '1 (cancerogeni o possibili cancerogeni)'])
plt.xlabel('Classe di cancerogenicità')
plt.ylabel('Count')
plt.title('Distribuzione delle classi di cancerogenicità')
plt.show()

non_classificato = df_clean[df_clean["carcinogenicity_score"] == 0]
cancerogeno = df_clean[df_clean["carcinogenicity_score"] == 1]
print(f"Numero di non classificati: {len(non_classificato)}")
print(f"Numero di cancerogeni / possibili cancerogeni: {len(cancerogeno)}")
C:\Users\Maura\AppData\Local\Temp\ipykernel_7084\1491774361.py:3: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(x='carcinogenicity_score', data=df_clean, palette='pastel')
No description has been provided for this image
Numero di non classificati: 1147
Numero di cancerogeni / possibili cancerogeni: 643
In [11]:
df_numeriche = df_clean.select_dtypes(include=['number'])
print(df_numeriche.columns)
Index(['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE', 'JCHEM_NUMBER_OF_RINGS',
       'JCHEM_PHYSIOLOGICAL_CHARGE', 'JCHEM_POLAR_SURFACE_AREA',
       'JCHEM_REFRACTIVITY', 'JCHEM_ROTATABLE_BOND_COUNT',
       'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE', 'carcinogenicity_score'],
      dtype='object')

Correlazione tra ogni variabile numerica e la variabile target

In [12]:
from mlxtend.plotting import scatterplotmatrix
colss = ['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE', 'JCHEM_NUMBER_OF_RINGS',
       'JCHEM_PHYSIOLOGICAL_CHARGE', 'JCHEM_POLAR_SURFACE_AREA',
       'JCHEM_REFRACTIVITY', 'JCHEM_ROTATABLE_BOND_COUNT',
       'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE', 'carcinogenicity_score']

from mlxtend.plotting import heatmap    
cm = np.corrcoef(df_clean[colss].values.T)   # Calcolo coefficiente di correlazione tra colonne  # Converto Dataframe in array Numpy   # Traspongo array
plt.figure(figsize=(20, 20))
hm = heatmap(cm,                          # Matrice di correlazione come input
             row_names=colss,              # Imposto le etichette per le righe
             column_names=colss,
            cell_font_size=7)           # Imposto le etichette per le colonne

plt.xticks(fontsize=7, rotation=90)
plt.yticks(fontsize=7, rotation=0)
plt.show()
<Figure size 2000x2000 with 0 Axes>
No description has been provided for this image
In [13]:
df_clean.describe()
Out[13]:
moldb_average_mass JCHEM_ACCEPTOR_COUNT JCHEM_AVERAGE_POLARIZABILITY JCHEM_BIOAVAILABILITY JCHEM_DONOR_COUNT JCHEM_FORMAL_CHARGE JCHEM_GHOSE_FILTER JCHEM_LOGP JCHEM_MDDR_LIKE_RULE JCHEM_NUMBER_OF_RINGS JCHEM_PHYSIOLOGICAL_CHARGE JCHEM_POLAR_SURFACE_AREA JCHEM_REFRACTIVITY JCHEM_ROTATABLE_BOND_COUNT JCHEM_RULE_OF_FIVE JCHEM_VEBER_RULE carcinogenicity_score
count 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000
mean 297.962796 1.615084 23.508142 0.916760 0.543575 0.034637 0.339665 3.095456 0.040782 1.644693 -0.030726 33.713061 59.623390 2.037989 0.651397 0.660894 0.359218
std 171.065752 2.543066 16.139935 0.276322 1.399932 0.614640 0.473728 3.170604 0.197840 1.650588 0.680031 50.771995 42.072629 3.622134 0.476661 0.473538 0.479905
min 9.011100 0.000000 0.435901 0.000000 0.000000 -12.000000 0.000000 -14.529231 0.000000 0.000000 -5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 193.242300 0.000000 10.749738 1.000000 0.000000 0.000000 0.000000 0.580984 0.000000 0.000000 0.000000 0.000000 26.205600 0.000000 0.000000 0.000000 0.000000
50% 283.230350 0.000000 24.645218 1.000000 0.000000 0.000000 0.000000 3.010901 0.000000 2.000000 0.000000 17.070000 65.581200 1.000000 1.000000 1.000000 0.000000
75% 360.878000 3.000000 31.397646 1.000000 1.000000 0.000000 1.000000 5.567426 0.000000 3.000000 0.000000 52.600000 80.055600 3.000000 1.000000 1.000000 1.000000
max 1961.036000 28.000000 156.669938 1.000000 20.000000 10.000000 1.000000 18.436200 1.000000 18.000000 6.000000 627.070000 397.851700 48.000000 1.000000 1.000000 1.000000
In [14]:
print(dir(pn))
['Story', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'story']
In [15]:
# Calcoli riassuntivi
mean_mass = df_clean["moldb_average_mass"].mean()
mean_logp = df_clean["JCHEM_LOGP"].mean()
mean_refractivity = df_clean["JCHEM_REFRACTIVITY"].mean()
perc_exogenous = df_clean[df_clean["origin"] == "Exogenous"].shape[0] / df_clean.shape[0] * 100
perc_carc = df_clean[df_clean["carcinogenicity_score"] == 1].shape[0] / df_clean.shape[0] * 100

# Narrazione testuale
story_text = f"""
L'analisi del dataset ha rivelato alcune caratteristiche chiave delle molecole in relazione alla loro potenziale cancerogenicità.

📊 Complessivamente, il valore medio della massa molecolare è di circa {mean_mass:.1f} g/mol, indicando la presenza di molecole di dimensioni medio-alte nel dataset. Il valore medio di LogP è pari a {mean_logp:.2f}, suggerendo un grado di lipofilia moderato: molte molecole hanno quindi la potenzialità di attraversare facilmente le membrane cellulari. La rifrazione molare media, un indice della polarizzabilità elettronica, è {mean_refractivity:.2f}, coerente con la presenza di molecole complesse.

🧪 Dal punto di vista tossicologico, il dataset mostra che circa il {perc_carc:.1f}% delle molecole sono classificate come cancerogene o potenzialmente tali. Inoltre, il {perc_exogenous:.1f}% delle molecole ha origine esogena, sottolineando come molte di esse possano derivare da sostanze industriali, contaminanti ambientali o farmaci.

📈 Un'osservazione interessante riguarda le relazioni tra le variabili: molecole con massa molecolare elevata tendono ad avere anche valori alti di rifrazione, il che è chimicamente plausibile poiché una maggiore massa comporta una struttura più complessa e quindi più facilmente polarizzabile.

🧬 Le molecole cancerogene tendono a concentrarsi nella zona del grafico che mostra alti valori sia di massa che di rifrazione molare. Questo suggerisce che molecole più pesanti e polarizzabili possano avere una maggiore capacità di interazione con bersagli biologici, come DNA o proteine cellulari, aumentando il rischio di effetti mutageni.

💡 Tuttavia, è importante sottolineare che nessuna singola variabile è risultata fortemente predittiva della cancerogenicità. Piuttosto, il rischio sembra emergere da un insieme di caratteristiche strutturali, confermando la necessità di un approccio multivariato per una valutazione più accurata.

L'esplorazione dei dati, dunque, mostra che la cancerogenicità non dipende da un solo fattore, ma è il risultato di **interazioni sinergiche tra massa, polarità, stato fisico, lipofilia e origine molecolare**.
"""

# Storia
story = pn.Story(story_text)
print(story_text)
L'analisi del dataset ha rivelato alcune caratteristiche chiave delle molecole in relazione alla loro potenziale cancerogenicità.

📊 Complessivamente, il valore medio della massa molecolare è di circa 298.0 g/mol, indicando la presenza di molecole di dimensioni medio-alte nel dataset. Il valore medio di LogP è pari a 3.10, suggerendo un grado di lipofilia moderato: molte molecole hanno quindi la potenzialità di attraversare facilmente le membrane cellulari. La rifrazione molare media, un indice della polarizzabilità elettronica, è 59.62, coerente con la presenza di molecole complesse.

🧪 Dal punto di vista tossicologico, il dataset mostra che circa il 35.9% delle molecole sono classificate come cancerogene o potenzialmente tali. Inoltre, il 95.9% delle molecole ha origine esogena, sottolineando come molte di esse possano derivare da sostanze industriali, contaminanti ambientali o farmaci.

📈 Un'osservazione interessante riguarda le relazioni tra le variabili: molecole con massa molecolare elevata tendono ad avere anche valori alti di rifrazione, il che è chimicamente plausibile poiché una maggiore massa comporta una struttura più complessa e quindi più facilmente polarizzabile.

🧬 Le molecole cancerogene tendono a concentrarsi nella zona del grafico che mostra alti valori sia di massa che di rifrazione molare. Questo suggerisce che molecole più pesanti e polarizzabili possano avere una maggiore capacità di interazione con bersagli biologici, come DNA o proteine cellulari, aumentando il rischio di effetti mutageni.

💡 Tuttavia, è importante sottolineare che nessuna singola variabile è risultata fortemente predittiva della cancerogenicità. Piuttosto, il rischio sembra emergere da un insieme di caratteristiche strutturali, confermando la necessità di un approccio multivariato per una valutazione più accurata.

L'esplorazione dei dati, dunque, mostra che la cancerogenicità non dipende da un solo fattore, ma è il risultato di **interazioni sinergiche tra massa, polarità, stato fisico, lipofilia e origine molecolare**.

In [16]:
storia = pn.Story(df_clean, font="Verdana")
In [17]:
grafico = (storia
           .mark_bar()
           .encode(
               x='JCHEM_LOGP:Q',
               y='JCHEM_REFRACTIVITY:Q',
               color=alt.Color('moldb_average_mass',
                               scale=alt.Scale(scheme='viridis'))
           )
           .properties(
               title='Relazione tra massa molecolare, rifrazione e lipofilia',
               width=700,
               height=400
           )
          )
grafico
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
Out[17]:
In [18]:
# Accorpo le categorie più frequenti per visualizzarle meglio nella legenda successiva
top_labels = df_clean['locations_all'].value_counts().nlargest(25).index
df_clean['loc_simplified'] = df_clean['locations_all'].apply(
    lambda x: x if x in top_labels else 'Altri'
)


# Grafico base
base = alt.Chart(df_clean).mark_point().encode(
    x='JCHEM_LOGP',
    y='JCHEM_REFRACTIVITY',
    color=alt.Color('loc_simplified:N',
    legend=alt.Legend(title='Localizzazione della molecola'),
    scale=alt.Scale(scheme='category10')
),
tooltip=['common_name', 'moldb_average_mass', 'JCHEM_LOGP', 'JCHEM_REFRACTIVITY', 'loc_simplified', 'carcinogenicity_label']
).properties(
    width=700,
    height=400,
    title='Relazione tra numero di anelli ciclici, rifrazione e lipofilia'
)

# Linea verticale
line = alt.Chart(pd.DataFrame({'x': [3.09]})).mark_rule(
    color='red',
    strokeDash=[5, 5],
    strokeWidth=2
).encode(x='x:Q')

# Compongo grafico
final_chart = base + line
final_chart
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
C:\Users\Maura\anaconda\Lib\site-packages\altair\utils\core.py:395: FutureWarning: the convert_dtype parameter is deprecated and will be removed in a future version.  Do ``ser.astype(object).apply()`` instead if you want ``convert_dtype=False``.
  col = df[col_name].apply(to_list_if_array, convert_dtype=False)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\anaconda\Lib\site-packages\altair\vegalite\v5\api.py in ?(self, include, exclude)
   2154         # see https://github.com/ipython/ipython/issues/11038
   2155         try:
   2156             dct = self.to_dict()
   2157         except Exception:
-> 2158             utils.display_traceback(in_ipython=True)
   2159             return {}
   2160         else:
   2161             return renderers.get()(dct)

~\anaconda\Lib\site-packages\altair\vegalite\v5\api.py in ?(self, *args, **kwargs)
    846 
    847         # TopLevelMixin instance does not necessarily have to_dict defined
    848         # but due to how Altair is set up this should hold.
    849         # Too complex to type hint right now
--> 850         dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)  # type: ignore[misc]
    851 
    852         # TODO: following entries are added after validation. Should they be validated?
    853         if is_top_level:

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(self, validate, ignore, context)
    792                 k: v for k, v in kwds.items() if k not in list(ignore) + ["shorthand"]
    793             }
    794             if "mark" in kwds and isinstance(kwds["mark"], str):
    795                 kwds["mark"] = {"type": kwds["mark"]}
--> 796             result = _todict(
    797                 kwds,
    798                 context=context,
    799             )

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):
--> 340         return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
    341     elif hasattr(obj, "to_dict"):
    342         return obj.to_dict()
    343     elif isinstance(obj, np.number):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    334     """Convert an object to a dict representation."""
    335     if isinstance(obj, SchemaBase):
    336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
--> 338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):
    340         return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
    341     elif hasattr(obj, "to_dict"):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    333 def _todict(obj, context):
    334     """Convert an object to a dict representation."""
    335     if isinstance(obj, SchemaBase):
--> 336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):

~\anaconda\Lib\site-packages\altair\vegalite\v5\api.py in ?(self, *args, **kwargs)
   2516             # for easier specification of datum encodings.
   2517             copy = self.copy(deep=False)
   2518             copy.data = core.InlineData(values=[{}])
   2519             return super(Chart, copy).to_dict(*args, **kwargs)
-> 2520         return super().to_dict(*args, **kwargs)

~\anaconda\Lib\site-packages\altair\vegalite\v5\api.py in ?(self, *args, **kwargs)
    846 
    847         # TopLevelMixin instance does not necessarily have to_dict defined
    848         # but due to how Altair is set up this should hold.
    849         # Too complex to type hint right now
--> 850         dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)  # type: ignore[misc]
    851 
    852         # TODO: following entries are added after validation. Should they be validated?
    853         if is_top_level:

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(self, validate, ignore, context)
    792                 k: v for k, v in kwds.items() if k not in list(ignore) + ["shorthand"]
    793             }
    794             if "mark" in kwds and isinstance(kwds["mark"], str):
    795                 kwds["mark"] = {"type": kwds["mark"]}
--> 796             result = _todict(
    797                 kwds,
    798                 context=context,
    799             )

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):
--> 340         return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
    341     elif hasattr(obj, "to_dict"):
    342         return obj.to_dict()
    343     elif isinstance(obj, np.number):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    333 def _todict(obj, context):
    334     """Convert an object to a dict representation."""
    335     if isinstance(obj, SchemaBase):
--> 336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(self, validate, ignore, context)
    792                 k: v for k, v in kwds.items() if k not in list(ignore) + ["shorthand"]
    793             }
    794             if "mark" in kwds and isinstance(kwds["mark"], str):
    795                 kwds["mark"] = {"type": kwds["mark"]}
--> 796             result = _todict(
    797                 kwds,
    798                 context=context,
    799             )

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):
--> 340         return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
    341     elif hasattr(obj, "to_dict"):
    342         return obj.to_dict()
    343     elif isinstance(obj, np.number):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    334     """Convert an object to a dict representation."""
    335     if isinstance(obj, SchemaBase):
    336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
--> 338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):
    340         return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
    341     elif hasattr(obj, "to_dict"):

~\anaconda\Lib\site-packages\altair\utils\schemapi.py in ?(obj, context)
    333 def _todict(obj, context):
    334     """Convert an object to a dict representation."""
    335     if isinstance(obj, SchemaBase):
--> 336         return obj.to_dict(validate=False, context=context)
    337     elif isinstance(obj, (list, tuple, np.ndarray)):
    338         return [_todict(v, context) for v in obj]
    339     elif isinstance(obj, dict):

~\anaconda\Lib\site-packages\altair\vegalite\v5\schema\channels.py in ?(self, validate, ignore, context)
     43                 # We still parse it out of the shorthand, but drop it here.
     44                 parsed.pop('type', None)
     45             elif not (type_in_shorthand or type_defined_explicitly):
     46                 if isinstance(context.get('data', None), pd.DataFrame):
---> 47                     raise ValueError(
     48                         'Unable to determine data type for the field "{}";'
     49                         " verify that the field name is not misspelled."
     50                         " If you are referencing a field from a transform,"

ValueError: Unable to determine data type for the field "carcinogenicity_label"; verify that the field name is not misspelled. If you are referencing a field from a transform, also confirm that the data type is specified correctly.
Out[18]:
alt.LayerChart(...)

🔬 Cosa ci racconta questo grafico?¶

In questo scatter plot vediamo tracciata una relazione tra due proprietà chimico-fisiche fondamentali delle molecole:

  • Asse X – LogP: la lipofilia, ovvero quanto una molecola è solubile nei grassi rispetto all'acqua.
  • Asse Y – Rifrazione molare: un indicatore della polarizzabilità elettronica e quindi della complessità strutturale.

Ogni punto rappresenta una molecola, colorata in base alla sua localizzazione cellulare prevalente (semplificata in categorie testuali), mentre una linea tratteggiata rossa verticale evidenziamediolore soglia di LogP (≈3.09), potenzialmente significativo nella distribuzione delle molecole f¶

🧬 Pattern interessant1. Molecole più idrofobe (a destra della linea rossa)** tendono a concentrarsi in zone con rifrazione intermedia-alta, suggerendo che molecole più complesse (e quindi più rifrangenti) sono anche più lipofile. Questo ha implicazioni tossicologiche: molecole lipofile attraversano facilmente le membrane biologiche e possono accumularsi nei tessuti.¶

  1. La maggior parte delle molecole è concentrata nell’area inferiore centrale, suggerendo che la chimica della vita (e dei composti analizzati) ruota intorno a molecole con moderata polarizzabilità e lipofilia.

  2. Distribuzione delle localizzazioni molecolari:

    • Le molecole localizzate nella membrana (viola chiaro) e nel citoplasma (rosso) sono le più diffuse e presenti in un’ampia gamma di LogP e rifrazione.
    • Le molecole presenti nel citoplasma (arancione) sono presenti in modesta misura in questo dataset, mentre quelle presenti nei mitocondri sono più rare.
    • Le molecole classificate come "Altri" si concentrano in un’area ristretta ma densa a bassa rifrazione e logP vicino allo zero.
    • Alcune localizzazioni rare, come giunzioni cellulari o membrane specializzate, si collocano ai margini dello spazio chimico, rappresentando outlier potenzialmente interessanti.enmnteressanti.

🧠 Interpretazione biologico-tossicologica¶

🔹 Molecole lipofile e altamente rifrangenti potrebbero avere un rischio maggiore di cancerogenicità, poiché:

  • penetrano più facilmente nelle cellule,
  • possono legarsi in modo aspecifico a proteine o DNA,
  • resistono ai processi di degradazione.

🔹 D'altra parte, la loro localizzazione può suggerire una funzionalità biologica cruciale (es. localizzazione nelle membrane, nel citoplasma, nei compartimenti extracellarkdown da inserire direttamente in Streamlit! Vuoi?

In [19]:
# Seleziono le colonne numeriche, escludendo target
df_numeriche = df_clean.select_dtypes(include=[np.number]).drop(columns='carcinogenicity_score')

# Calcolo la correlazione con il target
corr = df_clean[df_numeriche.columns].corrwith(df_clean['carcinogenicity_score'])

print(corr)

# Variabili con correlazione significativa (es. > 0.1)
selezionate = corr[abs(corr) > 0.1].index.tolist()
print("Variabili utili:", selezionate)
moldb_average_mass              0.114990
JCHEM_ACCEPTOR_COUNT           -0.185265
JCHEM_AVERAGE_POLARIZABILITY   -0.058625
JCHEM_BIOAVAILABILITY          -0.107389
JCHEM_DONOR_COUNT              -0.136881
JCHEM_FORMAL_CHARGE             0.001381
JCHEM_GHOSE_FILTER             -0.308332
JCHEM_LOGP                      0.180770
JCHEM_MDDR_LIKE_RULE           -0.083735
JCHEM_NUMBER_OF_RINGS          -0.103406
JCHEM_PHYSIOLOGICAL_CHARGE     -0.048374
JCHEM_POLAR_SURFACE_AREA       -0.189558
JCHEM_REFRACTIVITY             -0.058404
JCHEM_ROTATABLE_BOND_COUNT     -0.189539
JCHEM_RULE_OF_FIVE             -0.273308
JCHEM_VEBER_RULE                0.191967
dtype: float64
Variabili utili: ['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT', 'JCHEM_BIOAVAILABILITY', 'JCHEM_DONOR_COUNT', 'JCHEM_GHOSE_FILTER', 'JCHEM_LOGP', 'JCHEM_NUMBER_OF_RINGS', 'JCHEM_POLAR_SURFACE_AREA', 'JCHEM_ROTATABLE_BOND_COUNT', 'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE']
In [20]:
fig = px.scatter(df_clean, x='moldb_average_mass', y='carcinogenicity_score',
                 color='origin',  
                 size='JCHEM_ACCEPTOR_COUNT',  
                 hover_data=['JCHEM_LOGP', 'JCHEM_DONOR_COUNT'],
                 title='Relazione tra peso molecolare e carcinogenicità')
fig.show()


top_corr = corr[abs(corr) > 0.1]

# Converto in DataFrame
df_top_corr = top_corr.reset_index()

# Rinomino le due colonne
df_top_corr = df_top_corr.iloc[:, :2]
df_top_corr.columns = ['Variabile', 'Correlazione']

# Barplot con Plotly
fig = px.bar(df_top_corr, x='Variabile', y='Correlazione',
             title='Correlazione con carcinogenicity_score',
             labels={'Variabile': 'Variabile', 'Correlazione': 'Correlazione'})
fig.update_layout(xaxis_tickangle=45)
fig.show()


fig = px.scatter(df_clean, x='moldb_average_mass', y='carcinogenicity_score',
                 color='state', 
                 size='JCHEM_POLAR_SURFACE_AREA', 
                 hover_data=['JCHEM_LOGP', 'JCHEM_POLAR_SURFACE_AREA'],
                 title='Relazione tra peso molecolare e carcinogenicità')
fig.show()
In [21]:
# Impostazioni grafiche
sns.set(style="whitegrid", palette="muted", font_scale=1.2)

# Variabili molecolari da analizzare
vars_molecolari = ['moldb_average_mass', 'JCHEM_LOGP', 'state', 'origin']
In [22]:
# 1️⃣ Istogrammi / Density plot per ogni variabile molecolare
for col in vars_molecolari:
    plt.figure(figsize=(8,4))
    sns.histplot(data=df_clean, x=col, kde=True, hue='carcinogenicity_score', multiple="stack", palette='deep')
    plt.title(f'Distribuzione di {col} per classe di cancerogenicità')
    plt.show()

# ----------------------------------------------
# 2️⃣ Boxplot
for col in vars_molecolari:
    plt.figure(figsize=(8,4))
    sns.boxplot(data=df_clean, x='carcinogenicity_score', y=col, palette='deep')
    plt.title(f'{col} per classe di cancerogenicità')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_7084\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_7084\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_7084\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_7084\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
In [23]:
fig_box = px.scatter(df_clean, x='moldb_average_mass', y='JCHEM_REFRACTIVITY',
                 color='carcinogenicity_score',
                 hover_name='common_name',
                 hover_data=['state', 'origin', 'JCHEM_LOGP'])
plt.show()
In [24]:
# Pairplot per esplorare relazioni tra tutte le variabili molecolari
sns.pairplot(df_clean, vars=df_numeriche, hue='carcinogenicity_score', palette='deep')
plt.suptitle('Relazioni tra variabili molecolari e cancerogenicità', y=1.02)
plt.show()
No description has been provided for this image

✅ Considerazioni e conclusioni finali

L’analisi esplorativa condotta su questo dataset ci ha permesso di guardare dentro la chimica delle molecole con una lente statistica. E ciò che emerge è chiaro: non esiste un singolo "colpevole" della cancerogenicità, ma piuttosto una rete di fattori intrecciati che insieme concorrono a determinarla.

Iniziamo da un dato cruciale: il dataset è sbilanciato, con circa il doppio delle molecole non cancerogene rispetto a quelle cancerogene o potenzialmente tali. Questo riflette la realtà biologica ma impone attenzione nell’interpretazione dei dati.

La matrice di correlazione conferma che nessuna variabile numerica – da sola – spiega il fenomeno cancerogeno. Le correlazioni più alte, seppur deboli, sono positive: molecole con massa molecolare maggiore, superficie polare più estesa e rifrazione molare più elevata tendono ad avere un punteggio cancerogeno più alto. Questo ci suggerisce che molecole più grandi, più polari e strutturalmente complesse potrebbero avere maggiori probabilità di interagire con strutture biologiche sensibili, come il DNA.

Dal punto di vista chimico-fisico, queste variabili sono intercorrelate: più una molecola è grande, più tende ad avere legami multipli, più è polarizzabile, e più aumenta la sua rifrazione molare. Inoltre, molecole pesanti tendono ad avere più anelli aromatici o eterociclici, strutture spesso associate a potenziale mutageno. In sintesi, la complessità strutturale può tradursi in maggiore reattività biologica.

Anche la provenienza della molecola conta: le sostanze esogene, spesso derivate da sintesi industriale o inquinanti ambientali, mostrano una maggiore incidenza di classificazione cancerogena. Non è un caso: molte di queste molecole sono xenobiotici, cioè sostanze estranee all'organismo, che spesso sfuggono ai meccanismi di detossificazione o generano metaboliti reattivi.

E per quanto riguarda lo stato fisico? Da solo non basta a spiegare la cancerogenicità, ma in combinazione con la massa molecolare può diventare rilevante. Le molecole solide e pesanti sembrano avere un rischio maggiore. Questo potrebbe essere legato alla loro persistenza nell'ambiente, alla difficoltà di degradazione e alla tendenza ad accumularsi nei tessuti biologici.

Le forti correlazioni tra variabili strutturali, come massa, rifrazione, polarizzabilità e numero di anelli, ci raccontano un’altra verità: la tossicità è multiforme. Non può essere spiegata linearmente. Serve un approccio multivariato, integrato, sistemico.

Questa analisi dunque suggerisce che:

  • La cancerogenicità molecolare è il risultato di interazioni sinergiche tra molteplici caratteristiche: dimensione, polarità, forma, origine, persistenza in alcuni casi correlate proporzionalmente tra di loro.
  • Non esiste un unico predittore “magico”, ma un profilo molecolare complesso da interpretare nel suo insieme.
  • Questo riflette ciò che già la tossicologia molecolare ci insegna: la pericolosità di una sostanza nasce dalla sua struttura, dalle sue proprietà fisico-chimiche, e dalla sua capacità di interagire selettivamente con bersagli biologici.

Da qui, il passo successivo è naturale: per valutare in modo affidabile il rischio cancerogeno serve un approccio modellistico e predittivo, basato su dati multidimensionali e metodi avanzati.

L’esplorazione dei dati condotta in questo lavoro è solo il punto di partenza. Ma è già un passo fondamentale verso uno studio della cancerogenicità e delle sostanze nocive più trasparente, interpretabile e guidata dai dati.